]> dgit.raspbian.org Git - gnome-shell.git/commitdiff
status/keyboard: Adapt to external source of keyboard layout
authorJonas Ådahl <jadahl@gmail.com>
Fri, 3 Oct 2025 15:02:37 +0000 (17:02 +0200)
committerMarge Bot <marge-bot@gnome.org>
Thu, 29 Jan 2026 15:10:52 +0000 (15:10 +0000)
This adapts to API changes that allow an external source (e.g. remote
desktop API user) to control the current keyboard layout. This is done
in two ways:

If the external keyboard source sets the keymap as "locked", the
indicator is fixed to the short name of the external source, and will
change the indicator label when the layout index is changed accordingly.
The builtin input sources are not selectable, and the keyboard selection
keybinding is disabled.

If the external keyboard source doesn't set the keymap as "locked",
whenever the current keymap is from an external source, it's indicated
as a non-sensitive but selected input source. Selecting another keyboard
layout overrides the one selected by the external entity.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3901>

js/misc/keyboardManager.js
js/ui/status/keyboard.js

index 924caa8c881bdfaa944c55785eebe16b3ce0e3ce..5724156e19007230a5aa6a8edde4602eeec29f2c 100644 (file)
@@ -1,5 +1,8 @@
 import GLib from 'gi://GLib';
 import GnomeDesktop from 'gi://GnomeDesktop';
+import Meta from 'gi://Meta';
+
+import * as Signals from './signals.js';
 
 export const DEFAULT_LOCALE = 'en_US';
 export const DEFAULT_LAYOUT = 'us';
@@ -27,8 +30,10 @@ export function getKeyboardManager() {
     return _keyboardManager;
 }
 
-class KeyboardManager {
+class KeyboardManager extends Signals.EventEmitter {
     constructor() {
+        super();
+
         // The XKB protocol doesn't allow for more than 4 layouts in a
         // keymap. Wayland doesn't impose this limit and libxkbcommon can
         // handle up to 32 layouts but since we need to support X clients
@@ -40,11 +45,18 @@ class KeyboardManager {
         this._localeLayoutInfo = this._getLocaleLayout();
         this._layoutInfos = {};
         this._currentKeymap = null;
+
+        global.backend.connect('keymap-changed', this._onKeymapChanged.bind(this));
+        global.backend.connect('keymap-layout-group-changed', this._onKeymapLayoutGroupChanged.bind(this));
+        global.backend.connect('reset-keymap-description',
+            () => this._ourKeymapDescription);
+        global.backend.connect('reset-keymap-layout-index',
+            () => this._current.groupIndex);
     }
 
-    async _applyLayoutGroup(group) {
+    _updateCurrentKeymap(info) {
         const options = this._buildOptionsString();
-        const [layouts, variants] = this._buildGroupStrings(group);
+        const [layouts, variants] = this._buildGroupStrings(info.group);
         const model = this._xkbModel;
 
         if (this._currentKeymap &&
@@ -52,36 +64,68 @@ class KeyboardManager {
             this._currentKeymap.variants === variants &&
             this._currentKeymap.options === options &&
             this._currentKeymap.model === model)
-            return;
+            return false;
+
+        const displayNames = info.group.map(g => g.displayName);
+        const shortNames = info.group.map(g => g.shortName);
+        this._currentKeymap = {
+            layouts,
+            variants,
+            options,
+            model,
+            displayNames,
+            shortNames,
+        };
+        return true;
+    }
 
-        this._currentKeymap = {layouts, variants, options, model};
-        await global.backend.set_keymap_async(layouts, variants, options, model, null);
+    _createKeymapDescription() {
+        return Meta.KeymapDescription.new_from_rules(this._currentKeymap.model,
+            this._currentKeymap.layouts,
+            this._currentKeymap.variants,
+            this._currentKeymap.options,
+            this._currentKeymap.displayNames,
+            this._currentKeymap.shortNames
+        );
     }
 
-    async _applyLayoutGroupIndex(idx) {
-        await global.backend.set_keymap_layout_group_async(idx, null);
+    _onKeymapChanged() {
+        this._keymapDescription = global.backend.get_keymap_description();
+        this.emit('keymap-changed');
     }
 
-    async _doApply(info) {
-        await this._applyLayoutGroup(info.group);
-        await this._applyLayoutGroupIndex(info.groupIndex);
+    _onKeymapLayoutGroupChanged() {
+        this.emit('keymap-changed');
     }
 
-    apply(id) {
+    async _doApply(id) {
         const info = this._layoutInfos[id];
         if (!info)
             return;
 
-        if (this._current && this._current.group === info.group) {
-            if (this._current.groupIndex !== info.groupIndex)
-                this._applyLayoutGroupIndex(info.groupIndex).catch(logError);
-        } else {
-            this._doApply(info).catch(logError);
+        let recreate;
+        if (this._updateCurrentKeymap(info))
+            recreate = true;
+        else if (this.isExternal())
+            recreate = true;
+        else
+            recreate = false;
+
+        if (recreate)
+            this._ourKeymapDescription = this._createKeymapDescription();
+
+        if (recreate || !this._current || this._current.groupIndex !== info.groupIndex) {
+            await global.backend.set_keymap_async(
+                this._ourKeymapDescription, info.groupIndex, null);
         }
 
         this._current = info;
     }
 
+    apply(id) {
+        this._doApply(id).catch(logError);
+    }
+
     reapply() {
         if (!this._current)
             return;
@@ -94,9 +138,17 @@ class KeyboardManager {
         this._layoutInfos = {};
 
         for (const id of ids) {
-            const [found, , , layout, variant] = this._xkbInfo.get_layout_info(id);
-            if (found)
-                this._layoutInfos[id] = {id, layout, variant};
+            const [found, displayName, shortName, layout, variant] =
+                this._xkbInfo.get_layout_info(id);
+            if (found) {
+                this._layoutInfos[id] = {
+                    id,
+                    layout,
+                    variant,
+                    displayName,
+                    shortName,
+                };
+            }
         }
 
         let i = 0;
@@ -160,4 +212,28 @@ class KeyboardManager {
     get currentLayout() {
         return this._current;
     }
+
+    get shortName() {
+        const seat = global.stage.context.get_backend().get_default_seat();
+        const keymap = seat.get_keymap();
+        return keymap.get_current_short_name();
+    }
+
+    get displayName() {
+        const seat = global.stage.context.get_backend().get_default_seat();
+        const keymap = seat.get_keymap();
+        return keymap.get_current_display_name();
+    }
+
+    isLocked() {
+        return this._keymapDescription?.is_locked() ?? false;
+    }
+
+    isExternal() {
+        if (!this._keymapDescription)
+            return false;
+        if (!this._ourKeymapDescription)
+            return true;
+        return !this._keymapDescription.direct_equal(this._ourKeymapDescription);
+    }
 }
index 600a1cf578b4be8f47c21f1bfdec6526077e44f7..58b48133637134f49e46746ea08134c9842a9cf4 100644 (file)
@@ -25,8 +25,6 @@ class LayoutMenuItem extends PopupMenu.PopupBaseMenuItem {
     _init(displayName, shortName) {
         super._init();
 
-        this.setOrnament(PopupMenu.Ornament.NO_DOT);
-
         this.label = new St.Label({
             text: displayName,
             x_expand: true,
@@ -371,6 +369,7 @@ export class InputSourceManager extends Signals.EventEmitter {
 
         this._xkbInfo = KeyboardManager.getXkbInfo();
         this._keyboardManager = KeyboardManager.getKeyboardManager();
+        this._keyboardManager.connect('keymap-changed', this._keymapChanged.bind(this));
 
         this._ibusReady = false;
         this._ibusManager = IBusManager.getIBusManager();
@@ -426,6 +425,9 @@ export class InputSourceManager extends Signals.EventEmitter {
     }
 
     _switchInputSource(display, window, event, binding) {
+        if (this._keyboardManager.isLocked())
+            return;
+
         if (this._mruSources.length < 2)
             return;
 
@@ -459,6 +461,10 @@ export class InputSourceManager extends Signals.EventEmitter {
         this._keyboardManager.reapply();
     }
 
+    _keymapChanged() {
+        this.emit('keymap-changed');
+    }
+
     _updateMruSettings() {
         // If IBus is not ready we don't have a full picture of all
         // the available sources, so don't update the setting
@@ -888,7 +894,8 @@ class InputSourceIndicator extends PanelMenu.Button {
         this._inputSourceManager = getInputSourceManager();
         this._inputSourceManager.connectObject(
             'sources-changed', this._sourcesChanged.bind(this),
-            'current-source-changed', this._currentSourceChanged.bind(this), this);
+            'current-source-changed', this._currentSourceChanged.bind(this),
+            'keymap-changed', this._keymapChanged.bind(this), this);
         this._inputSourceManager.reload();
     }
 
@@ -904,47 +911,114 @@ class InputSourceIndicator extends PanelMenu.Button {
         this._showLayoutItem.visible = Main.sessionMode.allowSettings;
     }
 
-    _sourcesChanged() {
-        for (const i in this._menuItems)
-            this._menuItems[i].destroy();
-        for (const i in this._indicatorLabels)
-            this._indicatorLabels[i].destroy();
+    _createExternalSource(keyboardManager) {
+        const {displayName, shortName} = keyboardManager;
+        const is = new InputSource('external', 'external', displayName, shortName, -1);
+        is.locked = keyboardManager.isLocked();
+        return is;
+    }
 
-        this._menuItems = {};
-        this._indicatorLabels = {};
+    _getCurrentSource() {
+        const {keyboardManager} = this._inputSourceManager;
+        if (keyboardManager.isExternal())
+            return this._inputSources[0];
+        else
+            return this._inputSourceManager.currentSource;
+    }
+
+    _updateInputSources() {
+        const {keyboardManager} = this._inputSourceManager;
+        if (keyboardManager.isLocked()) {
+            const is = this._createExternalSource(keyboardManager);
+            this._inputSources = [is];
+            return;
+        }
 
-        let menuIndex = 0;
-        for (const i in this._inputSourceManager.inputSources) {
-            const is = this._inputSourceManager.inputSources[i];
+        const inputSources = [];
+        if (keyboardManager.isExternal())
+            inputSources.push(this._createExternalSource(keyboardManager));
+
+        const internalSources = this._inputSourceManager.inputSources;
+        inputSources.push(
+            ...Object.keys(internalSources).sort((a, b) => a - b).map(i => internalSources[i]));
+
+        this._inputSources = inputSources;
+    }
+
+    _addSourceIndicators() {
+        const inputSources = this._inputSources;
+        for (const i in inputSources) {
+            const is = inputSources[i];
 
             const menuItem = new LayoutMenuItem(is.displayName, is.shortName);
-            menuItem.connect('activate', () => is.activate(true));
+            if (is.type !== 'external')
+                menuItem.connect('activate', () => is.activate(true));
+            else
+                menuItem.sensitive = false;
+
+            if (is.locked)
+                menuItem.setOrnament(PopupMenu.Ornament.HIDDEN);
+            else
+                menuItem.setOrnament(PopupMenu.Ornament.NO_DOT);
 
             const indicatorLabel = new St.Label({
                 text: is.shortName,
                 visible: false,
             });
 
-            this._menuItems[i] = menuItem;
-            this._indicatorLabels[i] = indicatorLabel;
+            const indicatorIndex = this._calculateSourceIndicatorIndex(is);
+
+            this._menuItems[indicatorIndex] = menuItem;
+            this._indicatorLabels[indicatorIndex] = indicatorLabel;
             is.connect('changed', () => {
                 menuItem.indicator.set_text(is.shortName);
                 indicatorLabel.set_text(is.shortName);
             });
 
-            this.menu.addMenuItem(menuItem, menuIndex++);
+            this.menu.addMenuItem(menuItem, indicatorIndex);
             this._container.add_child(indicatorLabel);
         }
     }
 
+    _sourcesChanged() {
+        this._updateInputSources();
+
+        for (const i in this._menuItems)
+            this._menuItems[i].destroy();
+        for (const i in this._indicatorLabels)
+            this._indicatorLabels[i].destroy();
+
+        this._menuItems = {};
+        this._indicatorLabels = {};
+
+        this._addSourceIndicators();
+    }
+
+    _calculateSourceIndicatorIndex(source) {
+        const keyboardManager = this._inputSourceManager.keyboardManager;
+        return (keyboardManager.isExternal() ? 1 : 0) + source.index;
+    }
+
+    _setSourceAsActive(source) {
+        const index = this._calculateSourceIndicatorIndex(source);
+        if (!source.locked)
+            this._menuItems[index]?.setOrnament(PopupMenu.Ornament.DOT);
+        this._indicatorLabels[index]?.show();
+    }
+
+    _setSourceAsInactive(source) {
+        const index = this._calculateSourceIndicatorIndex(source);
+        if (!source.locked)
+            this._menuItems[index]?.setOrnament(PopupMenu.Ornament.NO_DOT);
+        this._indicatorLabels[index].hide();
+    }
+
     _currentSourceChanged(manager, oldSource) {
-        const nVisibleSources = Object.keys(this._inputSourceManager.inputSources).length;
-        const newSource = this._inputSourceManager.currentSource;
+        const nVisibleSources = Object.keys(this._inputSources).length;
+        const newSource = this._getCurrentSource();
 
-        if (oldSource) {
-            this._menuItems[oldSource.index].setOrnament(PopupMenu.Ornament.NO_DOT);
-            this._indicatorLabels[oldSource.index].hide();
-        }
+        if (oldSource)
+            this._setSourceAsInactive(oldSource);
 
         if (!newSource || (nVisibleSources < 2 && !newSource.properties)) {
             // This source index might be invalid if we weren't able
@@ -962,8 +1036,12 @@ class InputSourceIndicator extends PanelMenu.Button {
 
         this._buildPropSection(newSource.properties);
 
-        this._menuItems[newSource.index].setOrnament(PopupMenu.Ornament.DOT);
-        this._indicatorLabels[newSource.index].show();
+        this._setSourceAsActive(newSource);
+    }
+
+    _keymapChanged() {
+        this._sourcesChanged();
+        this._setSourceAsActive(this._getCurrentSource());
     }
 
     _buildPropSection(properties) {
@@ -1005,9 +1083,10 @@ class InputSourceIndicator extends PanelMenu.Button {
                 else
                     text = prop.get_label().get_text();
 
-                const currentSource = this._inputSourceManager.currentSource;
+                const currentSource = this._getCurrentSource();
                 if (currentSource) {
-                    const indicatorLabel = this._indicatorLabels[currentSource.index];
+                    const index = this._calculateSourceIndicatorIndex(currentSource);
+                    const indicatorLabel = this._indicatorLabels[index];
                     const graphemeClusters = this._getGraphemeClusters(text);
                     if (graphemeClusters.length > 0 && graphemeClusters.length < 3)
                         indicatorLabel.set_text(text);